import { isError } from 'lodash'
import BigNumber from 'bignumber.js'

import { logger } from '@pkg/logger'
import { nothrow, sleep } from '@pkg/promise'

import { Wallet } from '@app/wallet'
import { fromWei } from '@app/utils'
import * as notify from '@app/notify'
import { TokenDecoder } from '@app/decoder'
import { BalanceManager } from '@app/balances'
import { Blocks } from '@app/storage/block'
import { Receives } from '@app/storage/receive'

import * as config from '@config/server.json'

interface Trade {
    symbol?: string
    from: string
    to: string
    amount?: string
    data?: string
    hash: string
}

class Poller {
    public wallet: Wallet
    public balances: BalanceManager

    private started = false
    private decoder: TokenDecoder
    private nextBlockNumber: number = 0

    constructor(wallet: Wallet, balances: BalanceManager) {
        this.wallet = wallet
        this.balances = balances
        this.decoder = new TokenDecoder(this.wallet.tronWeb)
    }

    /**
     * 开始轮询区块
     */
    async startPolling() {
        if (this.started) {
            return
        }
        this.started = true
        const record = await Blocks.first()
        this.nextBlockNumber = record.number

        while (this.started) {
            if (!await this.scanNextBlock()) {
                await sleep(3000)
            }
        }
    }

    /**
     * 解析下个区块
     */
    private async scanNextBlock() {
        // 获取当前区块
        const tronWeb = this.wallet.tronWeb
        const currentBlock = (await nothrow(tronWeb.trx.getCurrentBlock())) as any
        if (isError(currentBlock)) {
            logger.info('[Poller] failed to get current block, %s', currentBlock)
            return false
        }

        // 区块是否确认
        const confirmations = 24
        const blockNumber = currentBlock.block_header.raw_data.number as number
        const trustedBlockNumber = blockNumber - confirmations
        if (trustedBlockNumber < this.nextBlockNumber) {
            return false
        }
        if (this.nextBlockNumber == 0) {
            this.nextBlockNumber = trustedBlockNumber
        }

        while (this.nextBlockNumber <= trustedBlockNumber) {
            // 获取区块数据
            const current = this.nextBlockNumber
            const block = (await nothrow(tronWeb.trx.getBlockByNumber(current))) as any
            if (isError(block)) {
                logger.info('[Poller] failed to get block by number, %s', block)
                return false
            }

            // 处理交易列表
            if (block.transactions) {
                const result = await nothrow(
                    this.processingTransactions(current, block.transactions as any[]))
                if (isError(result)) {
                    logger.info('[Poller] failed to parse transactions, %s', result)
                    return false
                }
            }

            // 更新扫描高度
            this.nextBlockNumber += 1
            await Blocks.update(this.nextBlockNumber)
            logger.debug('[Poller] scan block number: %d', current)
        }

        return true
    }

    /**
     * 获取交易列表
     * @params transaction 交易信息
     */
    private async getTrades(transaction: any) {
        const trades = new Array<Trade>()
        if (!transaction) {
            return trades
        }
        if (!transaction.raw_data) {
            return trades
        }

        const contract = transaction.raw_data.contract as any[]
        if (!contract || contract.length == 0) {
            return trades
        }

        const tronWeb = this.wallet.tronWeb
        contract.forEach((value) => {
            const data = value.parameter.value
            if (value.type === 'TransferContract') {
                trades.push({
                    symbol: 'TRX',
                    from: tronWeb.address.fromHex(data.owner_address),
                    to: tronWeb.address.fromHex(data.to_address),
                    amount: data.amount.toString(),
                    hash: transaction.txID
                })
            } if (value.type === 'TriggerSmartContract') {
                trades.push({
                    from: tronWeb.address.fromHex(data.owner_address),
                    to: tronWeb.address.fromHex(data.contract_address),
                    data: data.data,
                    hash: transaction.txID
                })
            }
        })
        return trades
    }

    /**
     * 处理Trade
     */
    private async processingTrade(blockNumber: number, trade: Trade) {
        if (!trade.symbol || !trade.amount) {
            return
        }

        // 更新余额缓存
        let fromLocal = false
        if (this.wallet.isLocalAccount(trade.from)) {
            fromLocal = true
            if (trade.symbol !== 'TRX') {
                await this.balances.updateBalance(trade.from, 'TRX')
            }
            await this.balances.updateBalance(trade.from, trade.symbol)
        }

        if (!this.wallet.isLocalAccount(trade.to)) {
            return
        } else {
            await this.balances.updateBalance(trade.to, trade.symbol)
        }

        // 保存收款记录
        await Receives.insert(trade.from, trade.to, trade.symbol, trade.amount, trade.hash)

        // 忽略内部转账
        if (fromLocal) {
            logger.warn('[Poller] transfer received, but ignore, from: %s, to: %s, symbol: %s, amount: %s, hash: %s',
                trade.from, trade.to, trade.symbol, trade.amount, trade.hash)
            return
        }

        logger.warn('[Poller] transfer received, from: %s, to: %s, symbol: %s, amount: %s, hash: %s',
            trade.from, trade.to, trade.symbol, trade.amount, trade.hash)

        // 发送转账通知
        let notifyURL = config.notify
        if (trade.symbol !== 'TRX') {
            const token = await this.wallet.getToken(trade.symbol)
            if (token.notify) {
                notifyURL = token.notify
            }
        }
        const message: notify.Message = {
            type: 'transaction',
            symbol: trade.symbol,
            from: trade.from,
            to: trade.to,
            hash: trade.hash,
            amount: trade.amount,
            blockNumber: blockNumber
        }
        await notify.send(notifyURL, message)
    }

    /**
     * 处理交易列表
     * @param transactions - 交易列表
     */
    private async processingTransactions(blockNumber: number, transactions: any[]) {
        const tronWeb = this.wallet.tronWeb
        for (let i = 0; i < transactions.length; i++) {
            // 检查交易状态
            const transaction = transactions[i]
            if (transaction.ret) {
                const ret = transaction.ret as any[]
                if (ret.length === 0 || !ret[0].contractRet || ret[0].contractRet !== 'SUCCESS') {
                    continue
                }
            }

            // 获取trade列表
            const trades = await this.getTrades(transaction)
            if (trades.length == 0) {
                continue
            }

            // 补全trade信息
            const fullTrades = new Array<Trade>()
            for (let j = 0; j < trades.length; j++) {
                const trade = trades[j]
                if (trade.symbol === 'TRX') {
                    trade.amount = tronWeb.fromSun(trade.amount) as string
                    fullTrades.push(trade)
                    continue
                }

                if (!trade.data) {
                    continue
                }

                const token = await this.wallet.getTokenByAddress(trade.to)
                if (!token || !token.decimals) {
                    continue
                }

                const event = this.decoder.decodeTransfer(trade.from, trade.data)
                if (!event) {
                    continue
                }
                trade.from = event.from
                trade.to = event.to
                trade.symbol = token.symbol
                trade.amount = fromWei(new BigNumber(event.amount), token.decimals).toString()
                fullTrades.push(trade)
            }

            // 处理有效trade
            for (let j = 0; j < fullTrades.length; j++) {
                const trade = fullTrades[j]
                await this.processingTrade(blockNumber, trade)
            }
        }
    }
}

export { Poller }
